﻿//************************************************************************************************************
// CoNatural.Data.dll
// Author: Roger Torres
//************************************************************************************************************
// You can use, modify, and distribute the source code and executable programs based on the source code.
// This source code is provided "as is" and without warranties as to performance or merchantability. 
// The author and/or distributors of this source code may have made statements about this source code. 
// Any such statements do not constitute warranties and shall not be relied on by the user in deciding 
// whether to use this source code. This source code is provided without any express or implied warranties 
// whatsoever. Because of the diversity of conditions and hardware under which this source code may be used, 
// no warranty of fitness for a particular purpose is offered. The user is advised to test the source code 
// thoroughly before relying on it. The user must assume the entire risk of using the source code.
//************************************************************************************************************

using System;
using System.Collections.Generic;
using System.Data;
using System.Threading;
using CoNatural.Binders;

namespace CoNatural.Data.Binders {
	/// <summary>
	/// Base data binder for CoNatural.Data commands.
	/// </summary>
	public class DataBinderBase : IDataBinder {
		public IDbProvider DbProvider { get; private set; }
		public ICommandScriptProvider ScriptProvider { get; private set; }

		/// <summary>
		///  Command binder base class
		/// </summary>
		public class CommandBinderBase : ICommandBinder {
			private IBinder inBinder;
			private IBinder outBinder;

			internal CommandBinderBase(string dbName, IBinder inBinder, IBinder outBinder) {
				DbName = dbName;
				this.inBinder = inBinder;
				this.outBinder = outBinder;
			}

			#region ICommandBinder Members

			public string DbName { get; private set; }
			
			public IDbCommand CreateDbCommand(IDbProvider dbProvider, ICommand command) { 
				return (IDbCommand)inBinder.Bind(command, dbProvider); 
			}
			
			public void CopyOutput(IDbCommand dbCommand, ICommand command) { 
				if(outBinder != null) 
					outBinder.Bind(dbCommand, command); 
			}

			#endregion
		}

		// cache binders for reuse within same connection.
		private Dictionary<int, IBinder> binderCache = new Dictionary<int, IBinder>();
		private Dictionary<int, ICommandBinder> commandBinderCache = new Dictionary<int, ICommandBinder>();
		private ReaderWriterLock cacheLock = new ReaderWriterLock();

		// internal specialized binder factories
		private IBinderDelegateFactory modelBinderDelegateFactory;
		private IBinderDelegateFactory materBinderDelegateFactory;
		private IBinderDelegateFactory incmdBinderDelegateFactory;
		private IBinderDelegateFactory outcmdBinderDelegateFactory;
		private IBinderDelegateFactory scrptBinderDelegateFactory;

		public DataBinderBase(IDbProvider dbProvider, ICommandScriptProvider scriptProvider) {
			DbProvider = dbProvider;
			ScriptProvider = scriptProvider ?? new EmbeddedResourceCommandScriptProviderBase();
			modelBinderDelegateFactory = new ModelBinderDelegateFactory();
			materBinderDelegateFactory = new MaterializerBinderDelegateFactory();
			incmdBinderDelegateFactory = new InCommandBinderDelegateFactory(dbProvider);
			outcmdBinderDelegateFactory = new OutCommandBinderDelegateFactory(dbProvider);
			scrptBinderDelegateFactory = new ScriptCommandBinderDelegateFactory(dbProvider);
		}

		#region IDataBinder Members

		public void Bind(object model, ICommand command) {
			var binder = CreateModelBinderDelegates(model, command.GetType());
			binder.Bind(model, command);
		}

		public void Bind(ICommand command, object model) {
			var binder = CreateModelBinderDelegates(command, model.GetType());
			binder.Bind(command, model);
		}

		public IList<IMapping> Map(object model, ICommand command) {
			var binder = CreateModelBinderDelegates(model, command.GetType());
			return binder.Map(model);
		}

		public ICommandBinder CreateBinder(ICommand command) {
			return LocateOrCreateCommandBinder(command); 
		}

		public IBinder CreateMaterializer<R, T>(ReaderExecutionContext<R> context)
			where R : class, new()
			where T : class, new() {
			bool isScript = context.Command is Script;
			int key = isScript ? context.Command.GetScript().GetHashCode() : context.Command.GetType().GetHashCode();
			key = (key | typeof(T).GetHashCode()) + context.ResultIndex;
			return LocateOrCreateBinder(key, () => materBinderDelegateFactory.CreateBinder(context.Record, typeof(T)));
		}

		#endregion

		#region Factories

		private IBinder CreateModelBinderDelegates(object source, Type targetType) {
			var key = source.GetType().GetHashCode() | targetType.GetHashCode() >> 1;
			return LocateOrCreateBinder(key, () => modelBinderDelegateFactory.CreateBinder(source, targetType));
		}

		private IBinder LocateOrCreateBinder(int key, Func<IBinder> factoryLambda) {
			IBinder binder;
			try {
				cacheLock.AcquireReaderLock(Timeout.Infinite);
				if (!binderCache.TryGetValue(key, out binder)) {
					LockCookie lc = new LockCookie();
					try {
						lc = cacheLock.UpgradeToWriterLock(Timeout.Infinite);
						if (!binderCache.TryGetValue(key, out binder)) {
							binder = factoryLambda();
							binderCache.Add(key, binder);
						}
					}
					finally {
						cacheLock.DowngradeFromWriterLock(ref lc);
					}
				}
			}
			finally {
				cacheLock.ReleaseReaderLock();
			}
			return binder;
		}

		private ICommandBinder LocateOrCreateCommandBinder(ICommand command) {
			bool isScript = command is Script;
			int key = isScript ? command.GetScript().GetHashCode() : command.GetType().GetHashCode();
			ICommandBinder binder;
			try {
				cacheLock.AcquireReaderLock(Timeout.Infinite);
				if (!commandBinderCache.TryGetValue(key, out binder)) {
					LockCookie lc = new LockCookie();
					try {
						lc = cacheLock.UpgradeToWriterLock(Timeout.Infinite);
						if (!commandBinderCache.TryGetValue(key, out binder)) {
							if (isScript)
								binder = new CommandBinderBase("Script", scrptBinderDelegateFactory.CreateBinder(command, typeof(IDbCommand)), null); // Scripts don't support output parameters
							else
								binder = new CommandBinderBase(DbProvider.ScriptName(command), incmdBinderDelegateFactory.CreateBinder(command, typeof(IDbCommand)), outcmdBinderDelegateFactory.CreateBinder(null, command.GetType()));
							commandBinderCache.Add(key, binder);
						}
					}
					finally {
						cacheLock.DowngradeFromWriterLock(ref lc);
					}
				}
			}
			finally {
				cacheLock.ReleaseReaderLock();
			}
			return binder;
		}

		#endregion Factories
	}
}
